/*
 * Copyright 2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.pajamas.starter.integration.module.tx;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.pajamas.protocol.Builder;
import org.pajmas.daydream.building.AbstractEzBuilder;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.NoRollbackRuleAttribute;
import org.springframework.transaction.interceptor.RollbackRuleAttribute;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttributeSource;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author william
 * @since 2024/1/7
 */

public class TxAttrSrcBuilder extends AbstractEzBuilder<TransactionAttributeSource> {
    private final transient Map<String, TransactionAttribute> txAttrMap;

    private TxAttrSrcBuilder() {
        this.txAttrMap = Maps.newLinkedHashMap();
    }

    public static TxAttrSrcBuilder newBuilder() {
        return new TxAttrSrcBuilder();
    }

    public TxAttrBuilder newTxAttr(String... matchedNames) {
        return new TxAttrBuilder(this, matchedNames);
    }

    private void putTxAttrMap(TransactionAttribute txArr, String... matchNames) {
        if (txArr == null || ArrayUtils.isEmpty(matchNames)) {
            return;
        }
        Arrays.stream(matchNames).filter(StringUtils::isNotBlank).forEach(name -> this.txAttrMap.put(name, txArr));
    }

    @Override
    public TransactionAttributeSource internalBuild() {
        NameMatchTransactionAttributeSource txAttSrc = new NameMatchTransactionAttributeSource();
        if (MapUtils.isNotEmpty(this.txAttrMap)) {
            txAttSrc.setNameMap(this.txAttrMap);
        }
        return txAttSrc;
    }

    public interface Customizer extends AbstractEzBuilder.Customizer<TransactionAttributeSource, TxAttrSrcBuilder> {
    }

    public static class TxAttrBuilder implements Builder<TransactionAttribute> {
        private final transient TxAttrSrcBuilder pBuilder;
        private final transient String[] matchNames;
        private transient Propagation propagation;
        private transient Isolation isolation;
        private transient Integer timeout;
        private transient Boolean readOnly;
        private transient Class<? extends Throwable>[] rollbackForClasses;
        private transient String[] rollbackForClassNames;
        private transient Class<? extends Throwable>[] noRollbackForClasses;
        private transient String[] noRollbackForClassNames;

        private TxAttrBuilder(TxAttrSrcBuilder pBuilder, String... matchNames) {
            this.pBuilder = pBuilder;
            this.matchNames = matchNames;
        }

        public TxAttrBuilder propagation(Propagation propagation) {
            this.propagation = propagation;
            return this;
        }

        public TxAttrBuilder propagation(String enumName) {
            this.propagation = Propagation.valueOf(enumName);
            return this;
        }

        public TxAttrBuilder isolation(Isolation isolation) {
            this.isolation = isolation;
            return this;
        }

        public TxAttrBuilder isolation(String enumName) {
            this.isolation = Isolation.valueOf(enumName);
            return this;
        }


        public TxAttrBuilder timeout(Integer timeout) {
            this.timeout = timeout;
            return this;
        }

        public TxAttrBuilder readOnly(Boolean readOnly) {
            this.readOnly = readOnly;
            return this;
        }

        public TxAttrBuilder rollbackForClasses(Class<? extends Throwable>... rollbackForClasses) {
            this.rollbackForClasses = rollbackForClasses;
            return this;
        }

        public TxAttrBuilder rollbackForClassNames(String... rollbackForClassNames) {
            this.rollbackForClassNames = rollbackForClassNames;
            return this;
        }

        public TxAttrBuilder noRollbackForClasses(Class<? extends Throwable>... noRollbackForClasses) {
            this.noRollbackForClasses = noRollbackForClasses;
            return this;
        }

        public TxAttrBuilder noRollbackForClassNames(String... noRollbackForClassNames) {
            this.noRollbackForClassNames = noRollbackForClassNames;
            return this;
        }

        @Override
        public TransactionAttribute build() {
            RuleBasedTransactionAttribute txArr = new RuleBasedTransactionAttribute();
            txArr.setPropagationBehavior(ObjectUtils.firstNonNull(this.propagation, Propagation.REQUIRED).value());
            txArr.setIsolationLevel(ObjectUtils.firstNonNull(this.isolation, Isolation.DEFAULT).value());
            txArr.setTimeout(ObjectUtils.firstNonNull(this.timeout, TransactionDefinition.TIMEOUT_DEFAULT));
            txArr.setReadOnly(ObjectUtils.firstNonNull(this.readOnly, false));
            List<RollbackRuleAttribute> rollbackRules = Lists.newArrayList();
            this.placeRollbackRuleAttribute(rollbackRules, true, this.rollbackForClasses);
            this.placeRollbackRuleAttribute(rollbackRules, false, this.noRollbackForClasses);
            this.placeRollbackRuleAttribute(rollbackRules, true, this.rollbackForClassNames);
            this.placeRollbackRuleAttribute(rollbackRules, false, this.noRollbackForClassNames);
            txArr.setRollbackRules(rollbackRules);

            this.pBuilder.putTxAttrMap(txArr, this.matchNames);

            return txArr;
        }

        private void placeRollbackRuleAttribute(List<RollbackRuleAttribute> rollbackRules, boolean isNot, Class<?
                extends Throwable>[] throwableClasses) {
            if (ArrayUtils.isEmpty(throwableClasses)) {
                return;
            }

            List<Class<? extends Throwable>> classes =
                    Arrays.stream(throwableClasses).filter(Objects::nonNull).distinct().collect(Collectors.toList());
            if (CollectionUtils.isEmpty(classes)) {
                return;
            }

            for (Class<? extends Throwable> aClass : classes) {
                RollbackRuleAttribute rollbackRuleAttribute = isNot ? new RollbackRuleAttribute(aClass) :
                        new NoRollbackRuleAttribute(aClass);
                rollbackRules.add(rollbackRuleAttribute);
            }
        }

        private void placeRollbackRuleAttribute(List<RollbackRuleAttribute> rollbackRules, boolean isNot,
                                                String[] throwableStrings) {
            if (ArrayUtils.isEmpty(throwableStrings)) {
                return;
            }

            List<String> strings =
                    Arrays.stream(throwableStrings).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
            if (CollectionUtils.isEmpty(strings)) {
                return;
            }

            for (String className : strings) {
                RollbackRuleAttribute rollbackRuleAttribute = isNot ? new RollbackRuleAttribute(className) :
                        new NoRollbackRuleAttribute(className);
                rollbackRules.add(rollbackRuleAttribute);
            }
        }
    }
}
